/*
 * Copyright 2011 Angel Sanadinov
 *
 * This file is part of VBox WMI.
 *
 * VBox WMI is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * VBox WMI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with VBox WMI.  If not, see <http://www.gnu.org/licenses/>.
 */

package Managers;

import Beans.UserBean;
import Beans.PermissionsBean;
import Beans.VirtualResourceBean;
import Beans.Requests.AuthorizationRequest;
import java.util.Map;
import java.util.Collections;
import Utilities.Constants;
import Utilities.CacheMap;
import Utilities.ConfigurationReader;
import Utilities.PermissionsConstants;
import Utilities.Interfaces.Controllable;
import Utilities.Interfaces.SelfValidating;
import com.sun.xml.ws.commons.virtualbox_3_2.IMedium;
import com.sun.xml.ws.commons.virtualbox_3_2.IVirtualBox;
import javax.xml.ws.WebServiceException;

/**
 * Manager for the permissions need for certain actions. <br><br>
 *
 * For performance reasons, a cache is used to store recently accessed virtual resources.
 *
 * @see PermissionsConstants Permissions Constants
 *
 * @author Angel Sanadinov
 */
public class PermissionsManager implements SelfValidating, Controllable
{
    private DatabaseManager databaseManager; //reference to the database manager
    private LogsManager logsManager;         //reference to the logs manager
    private SessionsManager sessionsManager; //reference to the sessions manager
    private VBoxConnectionsManager connectionsManager; //reference to the connections manager
    private Map<String, VirtualResourceBean> virtualResourcesCache; //cache for recently accessed resources

    /**
     * Constructs the permissions manager using the specified cache size.
     *
     * @param database reference to the database manager
     * @param logger reference to the logs manager
     * @param sessions reference to the sessions manager
     * @param connections reference to the VirtualBox connections manager
     * @param configuration configuration parameters for the manager
     *
     * @see DatabaseManager Database Manager
     * @see LogsManager Logs Manager
     * @see SessionsManager Sessions Manager
     * @see VBoxConnectionsManager Connections Manager
     */
    public PermissionsManager(DatabaseManager database, LogsManager logger, SessionsManager sessions,
            VBoxConnectionsManager connections, ConfigurationReader configuration)
    {
        this.databaseManager = database;
        this.logsManager = logger;
        this.sessionsManager = sessions;
        this.connectionsManager = connections;
        
        int cacheSize = 0;
        try
        {
            cacheSize = Integer.parseInt(configuration.getParameter(ConfigurationReader.PARAM_PERMISSIONS_CACHE_SIZE));
        }
        catch(NumberFormatException e)
        {
            logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_WARNING,
                                       "Permissions Manager: Failed to parse initialization parameter. "
                                       + "Default value ("+cacheSize+") for cacheSize will be used.");
        }
        
        this.virtualResourcesCache = Collections.synchronizedMap(new CacheMap<String, VirtualResourceBean>(cacheSize));
        this.logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_INFORMATION, "Permissions manager started...");
    }

    /**
     * Checks if the requested action is authorized based on the supplied
     * <code>AuthorizationRequest</code>.
     *
     * @param request the request object
     * @return <code>true</code> if the requested action is allowed or <code>false</code> otherwise
     *
     * @see AuthorizationRequest Authorization Request
     */
    public boolean isActionAuthorized(AuthorizationRequest request)
    {
        boolean result = false;

        //checks the validity of the request
        if(!request.isValid())
            return false;
        else
            ;

        //retrieves user data for the specified user from the sessions manager
        UserBean userData = sessionsManager.getUserData(request.getRequestorId());

        //checks if the user is logged in and if he is flagged as an owner
        if(userData == null)
            return false; //not logged in
        else if(userData.getAccountLevel() >= Constants.DB_USER_ACCOUNT_LEVEL_OWNER)
        {
            logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_DEBUG,
                                       "Action Authorized. User is owner: " + userData.getUserId());
            return true; //user is owner; has full access
        }
        else
            ;

        //checks if the requested action is concerned with user management or server management
        if(request.getRequestedAction() == PermissionsConstants.BASIC_ACTION_MANAGE_USER)
        {
            //checks if the user has the permissions to manage other users
            if(userData.getAccountLevel() == Constants.DB_USER_ACCOUNT_LEVEL_MANAGER)
            {
                logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_DEBUG,
                                           "Action Authorized. User is manager: " + userData.getUserId());
                return true;
            }
            else
                return false;
        }
        else if(request.getRequestedAction() == PermissionsConstants.BASIC_ACTION_MANAGE_SERVER)
            return false;//only WMI Owners are allowed to manage servers
        else
            ;
        
        //retrieves the permissions for the user
        PermissionsBean userPermissions =
                sessionsManager.getPermissionsForUserOnServer(request.getRequestorId(), request.getServerId());
        //retrieves the permissions for the resource
        VirtualResourceBean resource =
                getResource(request.getAffectedResourceId(), request.getRequestorId(),
                            request.getServerId(), request.getAffectedResourceType());

        int userPermission = 0;
        int resourcePermission = 0;

        //checks if all required permissions are available and valid
        if(userPermissions != null && userPermissions.isValid() && resource != null && resource.isValid()) 
        {
            int substrBegin = 0;//start of permission substring
            int substrEnd = 0;  //end of permission substring

            //determines which part of the permission to extract
            if(resource.getOwnerId() == request.getRequestorId()) //user is owner of resource
            {
                substrBegin = 0;
                substrEnd = 1;
            }
            else if(false /* checks groups */)  //group permissions; not implemented!
            {
                substrBegin = 1;
                substrBegin = 2;
            }
            else //global permission is to be used
            {
                substrBegin = 2;
                substrEnd = 3;
            }

            //extracts the proper permissions and parses them to integers
            userPermission = Integer.parseInt(
                    userPermissions.getResourcePermissions(request.getAffectedResourceType()).substring(substrBegin, substrEnd));
            resourcePermission = Integer.parseInt(
                    resource.getPermissions().substring(substrBegin, substrEnd));
        }
        else if(resource == null && request.getAffectedResourceId() == null)
        {
            //up to this point, the request is valid, but no resource is available, therefore
            //this is a request to create a new resource and only the user permissions on the server matter
            userPermission = Integer.parseInt(
                    userPermissions.getResourcePermissions(request.getAffectedResourceType()).substring(0, 1));
            //this is set only to maintain consistency and will not affect the final result of the check
            resourcePermission = userPermission;
        }
        else
            return false;

        //parses the requested action and retrieves the basic action
        int basicAction = parseAction(request.getRequestedAction());

        //checks that the requested action is valid
        //if the requested action is allowed, binary AND-ing the actual permissions
        //with the requested one should return the requested permissions
        //Ex: 101 & 100 = 100 ---- 5 (READ + EXECUTE) & 4 (READ) = 4 (READ)
        if((userPermission & basicAction) == basicAction
          && (resourcePermission & basicAction) == basicAction)
        {
            result = true;
            logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_DEBUG, 
                                       "Action Authorized. User has enough permissions: " + userData.getUserId());
        }
        else
            ;

        return result;
    }

    /**
     * Retrieves the resource from the database or the cache using the specified
     * parameters.
     *
     * @param resourceId the id of the resource
     * @param userId the id of the user that send the request
     * @param serverId the is of the server on which the resource resides
     * @param resourceType the type of the resource
     * @return the actual resource or <code>null</code> if the resource could not
     *         be retrieved
     *
     * @see Constants
     */
    private VirtualResourceBean getResource(String resourceId, int userId, int serverId, Constants.VirtualResourceType resourceType)
    {
        VirtualResourceBean resource = null;
        String currentResourceId = resourceId;
        String cacheResourceId = currentResourceId + "|" + serverId; //resource id in the cache

        //checks if the resource is a medium
        if(resourceType == Constants.VirtualResourceType.MEDIUM)
        {//this block checks if the medium is differencing and if so, it's base medium needs to be returned;
         //an operation failing here will not necessarily mean that the resource cannot be found in the
         //database, therefore the method should continue execution...
            try
            {
                //retrieves the virtualbox connection
                IVirtualBox vbox = connectionsManager.getVirtualBoxReference(serverId, userId);

                if(vbox != null)
                {
                    IMedium disk = vbox.getHardDisk(resourceId);

                    //checks if this is a base medium
                    if(disk != null && disk.getParent() != null) //this is only true for differencing children disks
                    {
                        currentResourceId = disk.getBase().getId();
                        cacheResourceId = currentResourceId + "|" + serverId; //sets the new cache id
                    }
                    else
                        ;
                }
                else
                    ;
            }
            catch(WebServiceException e){}
        }
        else
            ;

        //checks if the resource is in the cache
        if(virtualResourcesCache.containsKey(cacheResourceId))
            resource = virtualResourcesCache.get(cacheResourceId);
        else
        {
            //retrieves the resource from the database
            resource = databaseManager.getVirtualResource(currentResourceId, serverId, resourceType);

            //if the resource was retrieved, it is put in the cache
            if(resource != null)
                virtualResourcesCache.put(cacheResourceId, resource);
            else
                ;
        }

        return resource;
    }

    /**
     * Updates the resource cache using the specified data. <br><br>
     *
     * The method only removes the entry of the specified id from the cache. The next
     * time an authorization request is sent, the new resource data will be retrieved and
     * cached.
     *
     * <b>Note:</b> <i>This method should be called when resource data has been modified,
     *              otherwise the cached data will not be up-to-date with the database.</i>
     *
     * @param resourceId the id of the resource that was modified
     * @param serverId the id of the server on which the resource resides
     */
    public void updateResourceCache(String resourceId, int serverId)
    {
        String cacheResourceId = resourceId + "|" + serverId; //resource id in the cache

        //checks if the resource is in the cache
        if(virtualResourcesCache.containsKey(cacheResourceId))
            virtualResourcesCache.remove(cacheResourceId);//removes the cached entry
        else
            ;
    }

    /**
     * Checks the validity of the manager. <br><br>
     * The manager is valid only if the references to the other required managers
     * are set and if the resource cache is initialized.
     *
     * @return <code>true</code> if the manager is valid
     */
    @Override
    public boolean isValid()
    {
        if(databaseManager != null && logsManager != null && sessionsManager != null && virtualResourcesCache != null
                && connectionsManager != null)
            return true;
        else
            return false;
    }

    /**
     * Stops the manager and removes any associated resource references.
     */
    @Override
    public void stop()
    {
        logsManager.logSystemEvent(System.currentTimeMillis(), Constants.LOG_SEVERITY_INFORMATION, "Permissions manager stopping...");

        databaseManager = null;
        logsManager = null;
        sessionsManager = null;
        connectionsManager = null;
        virtualResourcesCache.clear();
        virtualResourcesCache = null;
    }

    /**
     * Parses the supplied action id based on the rules outlined in the constants
     * file, holding all actions. <br><br>
     *
     * <b>Note:</b> <i>This method should not be used for parsing basic actions
     *              or actions that deal with user or server manipulations. </i>
     *
     * @param actionId the id of the action to be parsed
     * @return the basic action id derived from the supplied id
     *
     * @see PermissionsConstants Action IDs
     * @see PermissionsConstants#BASIC_ACTION_EXECUTE
     * @see PermissionsConstants#BASIC_ACTION_WRITE
     * @see PermissionsConstants#BASIC_ACTION_READ
     */
    private int parseAction(int actionId)
    {
        //each complex id has a mask: Read(4000), Write(2000), Execute(1000);
        //by dividng by 1000 and retrieving the integer result, we end up with
        //1, 2 or 4, which are the basic actions Read(4), Write(2), Execute(1);
        //this is true only for valid ids
        return actionId / 1000;
    }
}
